Fix ML-DSA MakePublicKey derivation, verify guards, and ASN derivation testing - #10985
Fix ML-DSA MakePublicKey derivation, verify guards, and ASN derivation testing#10985stenslae wants to merge 4 commits into
Conversation
|
retest this please |
|
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10985
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 6 total — 4 posted, 2 skipped
Posted findings
- [High] wc_MlDsaKey_MakePublicKey mishandles buffers in the small-mem path (aSet, h, s1) —
wolfcrypt/src/wc_mldsa.c:10413-10665 - [Medium] wc_EccPrivateKeyDecode now unconditionally derives the public point —
wolfcrypt/src/asn.c:32722-32745 - [Low] Dead/empty guard branch before t0Scratch allocation —
wolfcrypt/src/wc_mldsa.c:10458-10467 - [Medium] Duplicated t = A o NTT(s1) + s2 kernel - extract shared helper with keygen —
wolfcrypt/src/wc_mldsa.c:10479-10635
Skipped findings
- [Medium] No test coverage for small-mem / cache-macro configurations of MakePublicKey
- [Medium] ML-DSA secret vectors left unzeroed in wc_MlDsaKey_MakePublicKey under SMALL_MEM + CACHE_PRIV_VECTORS
Review generated by Skoll via Claude/Codex
b6324ad to
b72a8ed
Compare
106f551 to
fa80057
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10985
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 3
3 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
fa80057 to
e292a3c
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10985
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 3
3 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
e292a3c to
ceb0a7d
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10985
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
ceb0a7d to
3d9668e
Compare
3d9668e to
2527a37
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10985
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10985
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 2
2 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
9e69c56 to
afe7a9f
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10985
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 23 total — 20 posted, 3 skipped
Posted findings
- [High] New ECC derive test's #if is missing four terms, so it fails in the opt-out, no-key-export and crypto-cb-only builds —
tests/api/test_asn.c:2854-2860 - [High] test_wc_MldsaDecisionCoverage2 PUBLIC_KEY_E expectation is not gated on the conditions that enable the KeyToDer auto-derive; fails with --enable-mldsa=sign,verify —
tests/api/test_mldsa.c:31734-31749 - [High] t0Scratch allocated without the 8-byte AVX2 tail slack, giving a 3-byte heap overflow on every private-only public-key derivation —
wolfcrypt/src/wc_mldsa.c:10694-10749 - [High] Fault-harden verify buffers packed back-to-back: 6-byte heap overflow and an always-failing self-check under AVX2 —
wolfcrypt/src/wc_mldsa.c:10777-10813 - [Medium] wc_MlDsaKey_KeyToDer lost its BAD_FUNC_ARG default: an unrecognised key->level now returns 0 —
wolfcrypt/src/wc_mldsa.c:13436-13488 - [Medium] Best-effort ECC public-key derivation is a no-op in every default (hardened) build —
wolfcrypt/src/asn.c:33143-33150 - [Medium] Unconditional extra base-point scalar multiply on every ECC private-key decode, including paths that discard the key immediately —
wolfcrypt/src/asn.c:33290-33294 - [Medium] EccDerivePubBestEffort is duplicated verbatim into asn_orig.c although asn_orig.c is #included into asn.c (same translation unit) —
wolfcrypt/src/asn_orig.c:7598-7654 - [Medium] Async ECC derive: retry loop keys off the wrong enable macro, and ecc_make_pub_ex no longer upgrades key->type on WC_PENDING_E —
wolfcrypt/src/asn.c:33152-33166 - [Medium] ECC OOM sub-test derives its baseline allocation count from a different DER blob, so the whole block can silently no-op —
tests/api/test_asn.c:2957-3001 - [Medium] WC_MLDSA_FAULT_HARDEN silently overrides WOLFSSL_MLDSA_NO_CHECK_KEY, and wc_mldsa.h re-derives the condition instead of consuming the new macro —
wolfcrypt/src/wc_mldsa.c:254-263 - [Medium] Widened CheckKey guard newly exposes a NULL key->a dereference to configurations where CheckKey previously did not exist —
wolfcrypt/src/wc_mldsa.c:12036-12066 - [Medium] Coverage gaps: PublicKeyToDer auto-derive, ML-DSA 65/87 OOM, cached-vector invalidation, MakePublicKey small-mem and fault-harden paths —
tests/api/test_mldsa.c:7761-8290 - [Medium] Under WOLF_CRYPTO_CB_FIND the devId gate is ineffective, so every SEC1 private-key decode fires an unsolicited EC_MAKE_PUB crypto callback —
wolfcrypt/src/asn.c:33136-33141 - [Low] Documentation gaps for the new decode-time derivation: undocumented macro, inaccurate guard comment, un-mirrored Japanese doxygen —
wolfcrypt/src/asn.c:33118 - [Low] MakePublicKey invalidates the private-vector cache in MAKE_KEY_SMALL_MEM builds where it never touched it —
wolfcrypt/src/wc_mldsa.c:10715-10720 - [Low] Unguarded struct pokes dereference key after a failed allocation or a skipped Init —
tests/api/test_mldsa.c:7787-7789 - [Low] test_mldsa_make_public_key_tr_mismatch comment claims to exercise the WC_MLDSA_FAULT_HARDEN recheck, which that path can never reach —
tests/api/test_mldsa.c:8248-8253 - [Low] ECC private-key decode now leaves an unblinded plaintext copy of the private scalar in key->ku (WOLFSSL_ECC_BLIND_K builds) —
wolfcrypt/src/asn.c:33153-33165 - [Info] *New PUBLIC_KEY_E early return in mldsa_verify_with_mu leaves res untouched, unlike every other error path in the function —
wolfcrypt/src/wc_mldsa.c:9837-9839
Skipped findings
- [Low] Coding-standard violations: comment punctuation, over-80-column lines and new bare scope blocks
- [Low] WC_RNG is not zeroed before use, so the unconditional wc_FreeRng at the end operates on an uninitialised stack struct when wc_InitRng fails
- [Info] Fault-harden branch documents a false invariant about matrix A after a failed CheckKey, and its aSet assignment is dead code
Review generated by Skoll via Claude/Codex
afe7a9f to
e5044af
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10985
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
No new issues found in the changed files. ✅
|
Jenkins retest this please |
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 24 total — 15 posted, 9 skipped
Posted findings
- [High] ecc_make_pub_ex no longer upgrades key->type on WC_PENDING_E, so async ECC verify with a private-only key never resumes —
wolfcrypt/src/ecc.c:5765-5769 - [Medium] EccDerivePubBestEffort retries with WC_ASYNC_FLAG_CALL_AGAIN on an operation documented as not re-callable —
wolfcrypt/src/asn.c:33222-33234 - [Medium] test_wc_EccPrivateKeyDecode_derive_pub guard omits !defined(WC_NO_RNG) and asserts behaviour the code deliberately skips there —
tests/api/test_asn.c:2785-2795,2856-2864 - [Medium] tests/suites.c guards add ML-DSA sign but not ML-DSA verify —
tests/suites.c:1329-1333,1362-1366 - [Medium] Public-key derivation now runs on every successful wc_EccPrivateKeyDecode, including probe/measure call sites that discard the key —
wolfcrypt/src/asn.c:33370-33374 - [Medium] Best-effort ECC public-key derivation is abandoned when the temporary RNG cannot be created, and instantiates an unused DRBG on every decode in SP-math builds —
wolfcrypt/src/asn.c:33196-33220, wolfcrypt/src/asn_orig.c:7630-7654 - [Low] EccDerivePubBestEffort duplicated verbatim (90 lines) in asn.c and asn_orig.c with no sync mechanism —
wolfcrypt/src/asn.c:33164-33253 - [Low] New user-settable macro WOLFSSL_NO_ECC_DERIVE_PUB_ON_DECODE not registered in .wolfssl_known_macro_extras —
wolfcrypt/src/asn.c:33167-33173 - [Low] *Public verify entry points return PUBLIC_KEY_E without zeroing res, unlike the internal guard they duplicate —
wolfcrypt/src/wc_mldsa.c:12175,12231,12293,12335 - [Low] ECC OOM loop never asserts the injected failure actually fired —
tests/api/test_asn.c:2978-3010 - [Low] ECC OOM guard omits WOLFSSL_MEM_FAIL_COUNT / WOLFSSL_FORCE_MALLOC_FAIL_TEST exclusions the ML-DSA harness has —
tests/api/test_asn.c:2785-2795,2943-2944 - [Low] ForceZero runs over uninitialized stack scratch on every early-error return of wc_MlDsaKey_MakePublicKey —
wolfcrypt/src/wc_mldsa.c:11757-11765 - [Low] Doxygen for the new/changed ML-DSA APIs is inconsistent with the implemented return codes —
doc/dox_comments/header_files/wc_mldsa.h:260-282,853-855 - [Low] wc_MlDsaKey_PublicKeyToDer() performs a full public-key derivation for the documented output==NULL size query —
wolfcrypt/src/wc_mldsa.c:14343-14360 - [Info] WOLFSSL_ECC_BLIND_K scrub of key->ku added in the ASN layer instead of ecc_make_pub_ex(), leaving every other make-pub caller unscrubbed —
wolfcrypt/src/asn.c:33247-33249, wolfcrypt/src/asn_orig.c:7681-7683
Skipped findings
- [Medium] WC_MLDSA_HAVE_CHECK_KEY's second arm is logically dead, and its condition is now hand-duplicated in three places that can drift
- [Low] devId skip in EccDerivePubBestEffort is ineffective under WOLF_CRYPTO_CB_FIND
- [Low] WOLFSSL_ECC_BLIND_K has no CI coverage, so the kua fix and the new key->ku scrub are never compiled
- [Low] test_mldsa_make_public_key_cache_invalidation's core assertions never run in any CI job that executes unit.test
- [Low] Roughly 30 added lines exceed the 80-column convention, including a 98-char comment duplicated in two files
- [Low] wc_MlDsaKey_MakePublicKey has no wc_dilithium_* legacy alias
- [Low] wc_mldsa.c gains an unconditional dependency on ConstantCompare(), which is compiled out under WOLFSSL_NO_CONST_CMP
- [Info] New ML-DSA public-key derivation scratch not registered with WOLFSSL_CHECK_MEM_ZERO
- [Info] WC_MLDSA_HAVE_CHECK_KEY and the fault-harden call site are gated on different macros; the second clause of WC_MLDSA_HAVE_CHECK_KEY is dead
Review generated by Skoll via Claude/Codex
e5044af to
bc2c6f2
Compare
wolfSSL-Fenrir-bot
left a comment
There was a problem hiding this comment.
Fenrir Automated Review — PR #10985
Scan targets checked: wolfcrypt-bugs, wolfcrypt-rs-bugs, wolfcrypt-src, wolfssl-bugs, wolfssl-src
Findings: 11
11 finding(s) posted as inline comments (see file-level comments below)
This review was generated automatically by Fenrir. Findings are non-blocking.
| /* Fail each allocation in turn. Whatever fails, decode must never | ||
| * report a derived public key it does not have: the key comes back | ||
| * either fully derived and correct, or still ECC_PRIVATEKEY_ONLY. */ | ||
| for (i = 1; EXPECT_SUCCESS() && (i <= totalAllocCount); i++) { |
There was a problem hiding this comment.
🔵 [Low] ECC OOM injection loop can pass vacuously · Weak or missing assertions
The loop is bounded by the counted totalAllocCount with no assertion that it is non-zero. In builds where the decode-plus-derive path allocates nothing in the armed window (e.g. ECC_CACHE_CURVE plus WOLFSSL_SP_NO_MALLOC, no WOLFSSL_SMALL_STACK), the body never executes and the test reports success without exercising any allocation-failure path.
Fix: Add ExpectIntGE(totalAllocCount, 1) before the loop, as mldsa_oom_derive_fail_level() does.
Frauschi
left a comment
There was a problem hiding this comment.
🐺 Skoll Code Review
Overall recommendation: REQUEST_CHANGES
Findings: 18 total — 8 posted, 10 skipped
Posted findings
- [High] Three undisclosed sword32-overflow fixes in ML-DSA small-mem arithmetic ship with no executing test —
wolfcrypt/src/wc_mldsa.c:8773 - [Medium] Under WOLF_CRYPTO_CB_FIND the devId gate in EccDerivePubBestEffort is still ineffective, so every SEC1 private-key decode fires an unsolicited EccMakePub callback —
wolfcrypt/src/asn.c:33196 - [Medium] Two sign-only ECC decode sites (pkcs7.c, ocsp.c) were not converted to derivePub=0 —
wolfcrypt/src/pkcs7.c:2150 - [Low] WC_MLDSA_HAVE_CHECK_KEY's second arm is unreachable and the condition is duplicated in two spellings —
wolfcrypt/src/wc_mldsa.c:262 - [Low] Doxygen for PublicKeyToDer/KeyToDer states the derivation is unconditional, but the code gates it on output != NULL —
doc/dox_comments/header_files/wc_mldsa.h:909 - [Low] placeholder byte relies on an undocumented internal contract of SetAsymKeyDer —
wolfcrypt/src/wc_mldsa.c:14463 - [Low] New key->ku scrub in ecc_make_pub_ex() is skipped on async WC_PENDING_E and never re-run —
wolfcrypt/src/ecc.c:5778-5791 - [Info] Redundant guard terms in tests/suites.c and the new ML-DSA test guards —
tests/suites.c:1330
Skipped findings
- [High] [BOT-DUPLICATE] Async derive failure leaves the key ECC_PRIVATEKEY with no public point, and the derivation runs unblinded
- [Medium] ML-DSA export functions now mutate a caller-owned key from inside wc_MakeCert/wc_SignCert
- [Medium] [BOT-DUPLICATE] mldsa_oom_derive_fail_level asserts an allocation count that is zero under WC_MLDSA_CACHE_PRIV_VECTORS
- [Medium] Coverage gaps around the new derivation opt-outs and error paths
- [Low] [BOT-DUPLICATE] Test comment claims a temporary RNG is used for blinding
- [Low] [BOT-DUPLICATE] ECC OOM sub-test can still pass vacuously, and carries a dead guard condition
- [Low] 23 added lines exceed the 80-column limit
- [Low] [BOT-DUPLICATE] internal.c justification comment is wrong (also static ECDH, and WOLFSSL_CHECK_SIG_FAULTS)
- [Info] PUBLIC_KEY_E replaces BAD_FUNC_ARG on four public verify entry points - needs a ChangeLog note
- [Info] MakePublicKey encode scratch holding encoded t0 is not registered with wc_MemZero_Add
Review generated by Skoll via Claude/Codex
| #ifdef WOLFSSL_MLDSA_SMALL | ||
| /* Reduce before invntt to avoid sword32 overflow, as in | ||
| * mldsa_calc_t_std()'s vec_red() call. */ | ||
| mldsa_poly_red(tt); |
There was a problem hiding this comment.
🟠 [High] Three undisclosed sword32-overflow fixes in ML-DSA small-mem arithmetic ship with no executing test
🚫 BLOCK test
Beyond the stated scope of the PR, the diff adds three new mldsa_poly_red() calls under WOLFSSL_MLDSA_SMALL, immediately before mldsa_invntt_full(): in mldsa_calc_t_small_mem (wc_mldsa.c:8773), in mldsa_sign_with_seed_mu (wc_mldsa.c:9926), and in the small-mem mldsa_verify_with_mu (wc_mldsa.c:11020). Each mirrors an existing mldsa_vec_red() in the corresponding standard path (wc_mldsa.c:8630, 9763, 10771) that the small-mem paths were missing. These are real latent correctness fixes: without the reduce, accumulated products can overflow sword32 before the inverse NTT, producing wrong keys and wrong signatures in WOLFSSL_MLDSA_SMALL + small-mem builds. The supporting guard widening at wc_mldsa.c:6298 and wc_mldsa.c:8289 (dropping !defined(WOLFSSL_MLDSA_VERIFY_SMALL_MEM)) exists solely to make mldsa_red/mldsa_poly_red compile for the new verify call site, confirming the additions are deliberate. Neither the PR description nor any of the four commit messages mentions them, no test fails without them, and no CI job would notice: --enable-mldsa=yes,small appears in exactly one entry (noasm-mldsa-align0, .github/workflows/wolfCrypt-Wconversion.yml:62) which sets no *_SMALL_MEM macro, and every ML-DSA small-mem entry (smallstack-noasm-small-mem, smallstack-intelasm-precalc, precalc-a-no-int128, verify-only-m32) is "check": false with --disable-crypttests, which .github/scripts/parallel-make-check.py:285 documents as skipping the make-check phase entirely. So WOLFSSL_MLDSA_SMALL combined with any small-mem variant is never built, and the small-mem paths that are built are never executed - this is exactly the code the PR rewrites wholesale into mldsa_calc_t_small_mem and then adds a second consumer for in wc_MlDsaKey_MakePublicKey.
Suggestion: Split the three mldsa_poly_red() additions into their own commit describing the overflow they fix, and add a CI entry that both builds and runs the combination:
{"name": "mldsa-small-small-mem", "minutes": 1,
"configure": ["--enable-cryptonly", "--enable-all-crypto", "--enable-mldsa=yes,small",
"CPPFLAGS=-DWOLFSSL_MLDSA_SIGN_SMALL_MEM -DWOLFSSL_MLDSA_VERIFY_SMALL_MEM -DWOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM"],
"check": true},
Recommendation: Separate the arithmetic fixes from the refactor so they are individually reviewable, and add at least one CI configuration combining WOLFSSL_MLDSA_SMALL with the small-mem macros that actually runs make check (the KATs in test_mldsa.c would catch a wrong t/w). Without a running config, both these fixes and the entire mldsa_calc_t_small_mem rewrite land unexercised.
There was a problem hiding this comment.
Did not reproduce any overflows, labeling this as a defensive change that follows convention. Added suggested CI config
| if (key->type != ECC_PRIVATEKEY_ONLY) { | ||
| return; | ||
| } | ||
|
|
There was a problem hiding this comment.
🟡 [Medium] Under WOLF_CRYPTO_CB_FIND the devId gate in EccDerivePubBestEffort is still ineffective, so every SEC1 private-key decode fires an unsolicited EccMakePub callback
💡 SUGGEST bug
Sites: wolfcrypt/src/asn.c:33196; wolfcrypt/src/asn.c:33197-33202
The helper skips derivation only when key->devId != INVALID_DEVID, which is exactly backwards for WOLF_CRYPTO_CB_FIND builds: there a find callback owns keys that carry INVALID_DEVID, so those keys fall straight through to wc_ecc_make_pub_ex, and ecc_make_pub_ex deliberately drops the same devId gate under FIND (ecc.c:5722-5726) to issue wc_CryptoCb_EccMakePub(key, pub). The in-tree comment justifying that FIND behaviour (ecc.c:5706-5720) reasons explicitly about the signing path - "Reaching the software signer means the device already declined to sign (the whole-sign offload wc_CryptoCb_EccSign ran first)" - and concludes a find callback would likewise decline. That precondition does not exist during SEC1 key parsing, which is a brand-new call context this PR introduces for that callback. A find callback that answers EccMakePub from a resident device key rather than the supplied key->k therefore yields the wrong public point on decode, silently and with a 0 return. Separately, every other device-first branch in ecc.c wraps the devId test in #ifndef WOLF_CRYPTO_CB_FIND (ecc.c:4775, 5721, 5916, 7035, 9150, 10622, 11232, 11822), so the helper also breaks the file's own convention. The identical omission is repeated in the new ML-DSA entry point at wolfcrypt/src/wc_mldsa.c:11453, whose comment explicitly claims key->k may be a device handle - precisely the FIND case. This is the third review round in which the WOLF_CRYPTO_CB_FIND half of this issue has been raised; only the WOLF_CRYPTO_CB_ONLY_ECC exclusion landed.
Also, at wolfcrypt/src/asn.c:33197-33202 (Low, bugs scan) — devId early-return in EccDerivePubBestEffort does not cover WOLF_CRYPTO_CB_FIND:
The helper skips derivation for device-tagged keys by testing key->devId != INVALID_DEVID. But ecc_make_pub_ex() drops exactly that gate when WOLF_CRYPTO_CB_FIND is defined (ecc.c:5721-5725): #ifndef WOLF_CRYPTO_CB_FIND / if ((err == MP_OKAY) && (key->devId != INVALID_DEVID)) / #else / if (err == MP_OKAY) / #endif. So in a WOLF_CRYPTO_CB_FIND build, the keys the helper deliberately lets through (devId == INVALID_DEVID) are precisely the ones that then reach wc_CryptoCb_EccMakePub() anyway. This PR therefore introduces a crypto-callback invocation on every private-only SEC1 key decode in such builds — a call that did not previously happen at decode time at all. The hazard is already documented in the existing comment at ecc.c:5705-5720 ("if the device ignores key->k ... it returns dG instead of the requested kG"); this change widens that exposure from the ECDSA sign path to ordinary key loading. This point was raised in an earlier review round and the exclusion was applied to the test guard in test_asn.c but not to the library guard.
Suggestion:
| #if defined(PLUTON_CRYPTO_ECC) || defined(WOLF_CRYPTO_CB) | |
| /* Don't derive host-side public points for device-backed keys. Under | |
| * WOLF_CRYPTO_CB_FIND any key may be device-backed - and ecc_make_pub_ex() | |
| * drops its own devId gate there - so skip the derivation entirely. */ | |
| #ifndef WOLF_CRYPTO_CB_FIND | |
| if (key->devId != INVALID_DEVID) | |
| #endif | |
| { | |
| return; | |
| } | |
| #endif |
Recommendation: Apply the #ifndef WOLF_CRYPTO_CB_FIND wrapper in both EccDerivePubBestEffort and wc_MlDsaKey_MakePublicKey. The early return in the helper is the only place this can be fixed - gating inside ecc_make_pub_ex would change the sign path too.
[wolfcrypt/src/asn.c:33197-33202] Skip the derivation outright under WOLF_CRYPTO_CB_FIND, since the devId gate cannot be relied on there:
#if defined(PLUTON_CRYPTO_ECC) || defined(WOLF_CRYPTO_CB)
/* Don't derive host-side public points for devId-tagged keys. Under
* WOLF_CRYPTO_CB_FIND, ecc_make_pub_ex() routes even INVALID_DEVID keys
* to a find callback, so skip the derivation entirely. */
#ifdef WOLF_CRYPTO_CB_FIND
return;
#else
if (key->devId != INVALID_DEVID) {
return;
}
#endif
#endif
Alternatively add !defined(WOLF_CRYPTO_CB_FIND) to the WOLFSSL_ECC_DERIVE_PUB_BEST_EFFORT guard at asn.c:33172-33179, mirroring the !defined(WOLF_CRYPTO_CB_ONLY_ECC) exclusion already present there.
| @@ -8670,7 +8670,10 @@ static int wc_PKCS7_KariParseRecipCert(WC_PKCS7_KARI* kari, const byte* cert, | |||
| else if (kari->direction == WC_PKCS7_DECODE) { | |||
There was a problem hiding this comment.
🟡 [Medium] Two sign-only ECC decode sites (pkcs7.c, ocsp.c) were not converted to derivePub=0
💡 SUGGEST convention
The PR systematically converted probe-only and sign-only decode sites to EccPrivateKeyDecodeEx(..., 0) (src/internal.c:33097, src/sniffer.c:2843, src/ssl_api_hs.c:1269, src/ssl_api_pk.c:2379, src/ssl_load.c:575, wolfcrypt/src/asn.c:9620, asn.c:10270, evp_pk.c:201, pkcs7.c:8676) but missed two meeting the same criteria. wc_PKCS7_ImportECC (wolfcrypt/src/pkcs7.c:2150) decodes a key used only for signing, and the cost is doubled: the immediately-following wc_ecc_check_key() now sees ECC_PRIVATEKEY instead of ECC_PRIVATEKEY_ONLY and runs the full public-key validation (_ecc_validate_public_key, ecc.c:11395), adding a second scalar multiply for the order check on every wc_PKCS7_EncodeSignedData. src/ocsp.c:2455 decodes ca->key.ecc, consumed only by OcspResponseEncode at src/ocsp.c:2766 for signing. Note the PKCS#7 site does gain a genuine fix from the derivation (its wc_ecc_check_key previously failed with ECC_INF_E on a private-only key), so if it is left deriving that should be stated as the reason rather than left as an oversight.
Suggestion:
| else if (kari->direction == WC_PKCS7_DECODE) { | |
| /* Skip the best-effort public point derivation done on decode: | |
| * this key is only used for signing, which needs the private | |
| * scalar alone. */ | |
| ret = EccPrivateKeyDecodeEx(pkcs7->privateKey, &idx, privKey, | |
| pkcs7->privateKeySz, 0); |
Recommendation: Convert src/ocsp.c:2455 to EccPrivateKeyDecodeEx(..., 0), and either convert wolfcrypt/src/pkcs7.c:2150 or add a comment stating the derivation is kept deliberately so wc_ecc_check_key succeeds on private-only keys.
| * as an opt-in fault check under WC_MLDSA_FAULT_HARDEN. Small-mem re-derives | ||
| * streaming instead, so it never pulls CheckKey (and full matrix A) in. | ||
| * One macro each so the guards can't drift. */ | ||
| #if defined(WOLFSSL_MLDSA_CHECK_KEY) || \ |
There was a problem hiding this comment.
🔵 [Low] WC_MLDSA_HAVE_CHECK_KEY's second arm is unreachable and the condition is duplicated in two spellings
💡 SUGGEST convention
Sites: wolfcrypt/src/wc_mldsa.c:262; wolfcrypt/src/wc_mldsa.c:258-268
WC_MLDSA_HAVE_CHECK_KEY is WOLFSSL_MLDSA_CHECK_KEY || (!NO_CHECK_KEY && WC_MLDSA_HAVE_MAKE_PUBLIC_KEY && WC_MLDSA_FAULT_HARDEN && !MAKE_KEY_SMALL_MEM). The second arm requires !WOLFSSL_MLDSA_NO_MAKE_KEY via WC_MLDSA_HAVE_MAKE_PUBLIC_KEY, and wolfssl/wolfcrypt/dilithium.h:317-323 derives WOLFSSL_MLDSA_PUBLIC_KEY and WOLFSSL_MLDSA_PRIVATE_KEY from exactly that, then auto-defines WOLFSSL_MLDSA_CHECK_KEY at dilithium.h:325-329 whenever both are set and WOLFSSL_MLDSA_NO_CHECK_KEY is not. The second arm is therefore strictly subsumed by the first, and WC_MLDSA_HAVE_CHECK_KEY is always identical to WOLFSSL_MLDSA_CHECK_KEY. The same condition is hand-copied into wolfssl/wolfcrypt/wc_mldsa.h:806-810 in a different form (expanding WC_MLDSA_HAVE_MAKE_PUBLIC_KEY inline and dropping !WOLFSSL_MLDSA_VERIFY_ONLY); the two are equivalent today only because VERIFY_ONLY force-defines NO_MAKE_KEY at dilithium.h:310-312 - precisely the fragile coupling the new macro was meant to remove. The PR comment One macro each so the guards can't drift is not achieved here.
Also, at wolfcrypt/src/wc_mldsa.c:258-268 (Info, bugs scan) — WC_MLDSA_HAVE_CHECK_KEY second arm is unreachable; small-mem still compiles CheckKey contrary to its comment:
The new WC_MLDSA_HAVE_CHECK_KEY macro is introduced with the stated goal that "Small-mem re-derives streaming instead, so it never pulls CheckKey (and full matrix A) in." That goal is not achieved, because the second arm of the macro can never be independently true. wolfssl/wolfcrypt/dilithium.h:317-330 already auto-defines WOLFSSL_MLDSA_CHECK_KEY whenever PUBLIC_KEY && PRIVATE_KEY && !WOLFSSL_MLDSA_NO_CHECK_KEY, and !WOLFSSL_MLDSA_NO_MAKE_KEY (required by WC_MLDSA_HAVE_MAKE_PUBLIC_KEY) implies both. So arm B implies arm A, WC_MLDSA_HAVE_CHECK_KEY is identically equal to WOLFSSL_MLDSA_CHECK_KEY, and every guard substitution in this PR is a semantic no-op. A WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM build still compiles wc_MlDsaKey_CheckKey() along with its full matrix-A allocation (up to ~57 KB for ML-DSA-87), which is the exact footprint the macro was added to avoid. The same condition is hand-copied into wolfssl/wolfcrypt/wc_mldsa.h:806-812 (headers cannot see the .c-only macro), creating two copies that must be kept in sync for no benefit. No runtime misbehavior — the small-mem streaming re-derivation is still what executes — but the code-size promise in the comment is not delivered.
Suggestion:
| #if defined(WOLFSSL_MLDSA_CHECK_KEY) || \ | |
| #ifdef WOLFSSL_MLDSA_CHECK_KEY | |
| #define WC_MLDSA_HAVE_CHECK_KEY | |
| #endif | |
| /* and revert wc_mldsa.h:806-810 to the plain `#ifdef WOLFSSL_MLDSA_CHECK_KEY` | |
| * it had before. */ |
Recommendation: Drop the dead second arm and revert the wc_mldsa.h prototype guard, so there is one condition rather than three spellings of the same thing.
[wolfcrypt/src/wc_mldsa.c:258-268] Either drop the redundant macro and keep using WOLFSSL_MLDSA_CHECK_KEY directly (removing the duplicated condition from wc_mldsa.h:806-812), or — if the small-mem size saving is actually wanted — make the exclusion real by having dilithium.h stop auto-defining WOLFSSL_MLDSA_CHECK_KEY under WOLFSSL_MLDSA_MAKE_KEY_SMALL_MEM, so that:
#define WC_MLDSA_HAVE_CHECK_KEY
can genuinely be false in a small-mem build. Until one of those happens, correct the comment so it does not claim a code-size reduction that is not delivered.
|
|
||
| Pass NULL as output to query the required buffer size. | ||
|
|
||
| If only the private key is set, the public key is derived and cached |
There was a problem hiding this comment.
🔵 [Low] Doxygen for PublicKeyToDer/KeyToDer states the derivation is unconditional, but the code gates it on output != NULL
🔧 NIT style
The added blocks for wc_MlDsaKey_PublicKeyToDer (doc line 909) and wc_MlDsaKey_KeyToDer (doc line 941) say "If only the private key is set, the public key is derived and cached in key" and add MEMORY_E/PUBLIC_KEY_E returns, with no mention that the size query is exempt. The implementation deliberately skips the derive when output == NULL (wc_mldsa.c:14398 and wc_mldsa.c:14513), precisely so a size query stays cheap and non-mutating, and neither error can be returned in that case. The equivalent block for wc_MlDsaKey_ExportPubRaw (doc line 787) is accurate because that function has no size-query mode.
Suggestion: If only the private key is set and output is non-NULL, the public key is
derived and cached in key; don't share key across threads during such a
call. A size query (output == NULL) never derives and never mutates key.
Recommendation: Qualify both blocks with the output != NULL condition so the documented error set matches the size-query path.
| byte placeholder = 0; | ||
| const byte* pub = (key->p != NULL) ? key->p : &placeholder; | ||
| #else | ||
| const byte* pub = key->p; |
There was a problem hiding this comment.
🔵 [Low] placeholder byte relies on an undocumented internal contract of SetAsymKeyDer
🔧 NIT style
Both wc_MlDsaKey_PublicKeyToDer (wc_mldsa.c:14463) and wc_MlDsaKey_KeyToDer (wc_mldsa.c:14522) pass a one-byte stack object as pubKey alongside a pubKeyLen of up to 2592, relying on the callee not reading it. I verified this holds: SetAsymKeyDerPublic (wolfcrypt/src/asn.c:13389) only reaches its XMEMCPY(output, pubKey, pubKeyLen) inside if ((ret == 0) && (output != NULL)), and SetAsymKeyDer (wolfcrypt/src/asn.c:34327) only touches pubKey inside if (ret == 0 && output != NULL) in both the template and non-template branches. But neither function's header comment states that contract, so a future change reading pubKey during the sizing pass would become a 2591-byte stack over-read with no compiler or test signal.
Suggestion: Add to the doxygen of SetAsymKeyDer() and SetAsymKeyDerPublic() in wolfcrypt/src/asn.c:
- @note pubKey must be non-NULL but is only dereferenced when output is
-
non-NULL; callers performing a size query may pass a placeholder.
Recommendation: Document the "pubKey is not read when output == NULL" contract at both callees so the placeholder trick is protected by more than a comment at the call site.
| key->type = ECC_PRIVATEKEY; | ||
| } | ||
|
|
||
| #ifdef WOLFSSL_ECC_BLIND_K |
There was a problem hiding this comment.
🔵 [Low] New key->ku scrub in ecc_make_pub_ex() is skipped on async WC_PENDING_E and never re-run
🔧 NIT
This PR adds a scrub of key->ku - the unblinded private scalar that ecc_get_k() reconstructs from k ^ kb under WOLFSSL_ECC_BLIND_K - at the end of ecc_make_pub_ex(). The scrub is explicitly skipped when the operation returns WC_PENDING_E, and nothing re-runs it: the PR's own comment two blocks above states the async result lands directly in key->pubkey and "the operation is not re-issued", and mp_forcezero(key->ku) appears nowhere else in ecc.c except wc_ecc_free() (line 8592). So on the async path the unblinded scalar stays resident in the key struct for the whole key lifetime, which is exactly what WOLFSSL_ECC_BLIND_K exists to prevent. The exposure is slightly wider than the sp_int digits: ecc_make_pub_sw() at line 5560 calls wc_mp_to_bigint_sz(ecc_get_k(key), &ecc_get_k(key)->raw, keySz) before IntelQaEccPointMul(), copying the plain scalar into the ku->raw heap bigint for the hardware; sp_forcezero() clears a->raw via wc_bigint_zero() (sp_int.c:5395), so the skipped scrub also skips that copy. Reachable with WOLFSSL_ASYNC_CRYPT + WC_ASYNC_ENABLE_ECC_KEYGEN + HAVE_INTEL_QA + WOLFSSL_ECC_BLIND_K, and this PR newly routes every default wc_EccPrivateKeyDecode() through ecc_make_pub_ex(), so the path is hit once per key decode rather than only at keygen. Rated Low: exploitation requires local memory-forensics access, and other ECC operations already leave ku populated on their own (pre-existing), so the marginal exposure over the status quo is small - but the newly added mitigation is incomplete on a reachable path.
Recommendation: Scrub key->ku on the async-completion path as well, so the pending case is covered rather than permanently skipped. Either call mp_forcezero(key->ku) from the async resume/completion handler that harvests key->pubkey, or - since ecc_get_k() recomputes ku from k ^ kb on every call and the QAT operation reads ku->raw, not ku->dp - clear ku->dp immediately after wc_mp_to_bigint_sz() and clear ku->raw in the completion path. At minimum, document in the comment that the async path defers the scrub to wc_ecc_free() so the gap is a known, deliberate limitation.
| @@ -1329,8 +1329,11 @@ int SuiteTest(int argc, char** argv) | |||
| #if defined(WOLFSSL_HAVE_SLHDSA) && \ | |||
| !defined(WOLFSSL_MLDSA_VERIFY_ONLY) && defined(WOLFSSL_HAVE_MLDSA) && \ | |||
There was a problem hiding this comment.
⚪ [Info] Redundant guard terms in tests/suites.c and the new ML-DSA test guards
🔧 NIT style
The new suites.c guard rationale checks out against tests/test-tls13-slhdsa-shake.conf and tests/test-tls13-slhdsa-sha2.conf: both sides do use ML-DSA-44 entity certs, so sign and verify are both needed, and the SLH-DSA root is only ever verified. WOLFSSL_MLDSA_NO_SIGN and WOLFSSL_MLDSA_NO_VERIFY are real macros. However, now that !defined(WOLFSSL_MLDSA_NO_SIGN) is present, the pre-existing !defined(WOLFSSL_MLDSA_VERIFY_ONLY) term is redundant - dilithium.h:310-312 force-defines WOLFSSL_MLDSA_NO_SIGN whenever VERIFY_ONLY is set. The same pattern appears in the new tests: test_mldsa_private_key_export_derives_public_key (tests/api/test_mldsa.c:7934) nests an inner !defined(WOLFSSL_MLDSA_NO_ASN1) && defined(WOLFSSL_MLDSA_PUBLIC_KEY) that merely restates its own outer guard, and several new guards spell out defined(WOLFSSL_MLDSA_PRIVATE_KEY) alongside !defined(WOLFSSL_MLDSA_NO_MAKE_KEY), which implies it via dilithium.h:321-323.
Suggestion: Drop !defined(WOLFSSL_MLDSA_VERIFY_ONLY) && from both suites.c guards, and drop the redundant inner #if at tests/api/test_mldsa.c:7934.
Recommendation: Trim the redundant terms so each guard states its condition once; this is the same drift risk as the WC_MLDSA_HAVE_CHECK_KEY finding.
|
My new skoll findings are on top of the Fenrir ones (removed the duplicates). |
Description
Refactored ML-DSA public key derivation to fix memory allocation issues in SMALL_MEM builds, and adds guards to ensure verification isn't attempted without a valid public key. Introduced ECC best-effort public key derivation when decoding private-only SEC1 keys. Added additional CI testing.
Notes:
public key is set.
wolfSSL/wolfssh#1120
Testing
Added test coverage.
Checklist